/* Universidad del Valle de Guatemala
 * CC2003 - Algoritmos y Estructuras de Datos
 * PROYECTO #3: PI-LISP
 * Fecha de Entrega: Lunes 21 de Noviembre de 2011
 */

/**
 * Clase que representa el Intérprete PI-LISP.
 * @authors José Antonio Bagur - 10275, Emilio José Miranda - 10411, Luis Antonio Siguenza - 10297
 */

/* SE IMPORTAN LOS PAQUETES UTILIZADOS EN LA CLASE */
import java.util.*;

public class Lisp {
    
        /* VARIABLES DE INSTANCIA */

        public static Hashtable<String, Atom> userdeFun = new Hashtable<String, Atom>();

        /**
         * Método que permite parsear una expresión ingresada al Intérprete.
         * @param des
         * @param listBol
         * @param isCl
         * @return expresión parseada por el Intérprete
         */
        public static Atom parseExpression(String exp, boolean listBol, boolean isCl) {
            exp = exp.trim();

            if (exp.equals("")) {
                // SE DEVUELVE NIL
                return new Atom();
            }        

            Atom respAtom = new Atom();
            StringTokenizer sep = new StringTokenizer(exp);
            String firstToken = sep.nextToken();         

            if ((sep.countTokens() == 0)) {
                if (firstToken.equals("()")) {
                    // SE DEVUELVE NIL
                    return new Atom();
                }          
            }

            if (firstToken.charAt(0) == '(') {
                int start2 = exp.indexOf('(') + 1;
                int finish2 = parentesisIndex(exp);

                exp = exp.substring(start2, finish2);
                respAtom = new Atom(new Lista());
                sep = new StringTokenizer(exp);

                if (listBol) {
                    if (!isCl) {
                        respAtom.List1.isOp = false;
                    } else {
                        respAtom.List1.isOp = true;
                    }
                } else {
                    respAtom.List1.isOp = true;
                }
            }

            if (!respAtom.isList) {
                return new Atom(exp);
            }

            sep = new StringTokenizer(exp);

            while (sep.hasMoreTokens()) {
                Atom actualAtom = new Atom(sep.nextToken());

                if ((actualAtom.startsWith("'(")) || (actualAtom.startsWith("("))) {
                    int start3 = exp.indexOf(actualAtom.toString());
                    exp = exp.substring(start3);
                    int finish3 = parentesisIndex(exp);
                    String inExpList = exp.substring(0, finish3 + 1);
                    Atom inAtom2 = new Atom();

                    boolean isDef = false;

                    if (actualAtom.startsWith("'(")) {
                        inAtom2 = parseExpression(inExpList, true, isCl);
                    } else {
                        if (respAtom.List1.size() > 0) {
                            if (respAtom.isOpList()) {
                                String opList = respAtom.List1.getOp().toString();

                                if ((opList.equalsIgnoreCase("DEFUN")) && (respAtom.List1.size() == 2)) {
                                    inAtom2 = parseExpression(inExpList, true, false);
                                    isDef = true;
                                }                            
                            }
                        }

                        if ((!isDef)) {
                            inAtom2 = parseExpression(inExpList, listBol, isCl);
                        }
                    }

                    respAtom.List1.addEnd(inAtom2);
                    String afterExp = exp.substring(finish3 + 1);
                    exp = afterExp;

                    if (!isDef) {
                        sep = new StringTokenizer(afterExp);
                    } else {
                        String nomFunc = respAtom.List1.getAtomIn(1).toString();
                        userdeFun.put(nomFunc, new Atom());

                        Atom opAtom = parseExpression(exp.trim(), false, false);
                        respAtom.List1.addEnd(opAtom);
                        sep = new StringTokenizer("");
                        userdeFun.remove(nomFunc);
                    }
                } else {
                    int start4 = exp.indexOf(actualAtom.toString());
                    exp = exp.substring(start4);

                    if (actualAtom.startsWith("'")) {
                        actualAtom = new Atom(actualAtom.toString().substring(1));
                    }

                    respAtom.List1.addEnd(actualAtom);
                }
            }

            return respAtom;
        }    

        /**
         * Metodo que permite evaluar una expresión ingresada por el usuario en el Interprete.
         * @return expresión evaluada por el Intérprete
         */
        public static Atom evaluateExp(Atom A) {
            Atom respA = new Atom();

            if (A.isNull) {
                return new Atom();
            }

            if (A.isList) {
                Lista dummyList1 = A.List1;

                if (dummyList1.isOp) {
                    String listOp = dummyList1.getOp().toString();
                    if (!listOp.equalsIgnoreCase("DEFUN")) {

                        for (int i = 0; i < dummyList1.size(); i++) {    
                            if (dummyList1.getAtomIn(i).isOpList()) {
                                Atom eList = evaluateExp(dummyList1.getAtomIn(i));
                                dummyList1.replaceFor(i, eList);
                            }
                        }
                    }
                } else {
                    return new Atom(dummyList1);
                }

                String dummyOp = dummyList1.getOp().toString();
                if (dummyOp.equals("+")) {
                    return sum(dummyList1);
                } else if (dummyOp.equals("*")) {
                    return multiply(dummyList1);
                } else if (dummyOp.equals("-")) {
                    return minus(dummyList1);
                } else if (dummyOp.equals("/")) {
                    return divide(dummyList1);
                } else if (dummyOp.equalsIgnoreCase("DEFUN")) {
                    return defun(dummyList1);
                } else if (userdeFun.containsKey(dummyOp)) {
                    return operateUFun(dummyList1);
                }
            } 
            return respA;
        }

        /**
         * Método que permite operar las funciones ingresadas por el usuario al Intérprete
         * @param L lista a verificar
         * @return operación del átomo a realizar
         */
        private static Atom operateUFun(Lista L) {
            int numPar = L.size() - 1;
            Lista listPar = userdeFun.get(L.getOp().toString()).List1.getAtomIn(0).List1;
            int numParFun = listPar.size();

            if (numParFun != numPar) {
                System.out.println("ERROR: LA FUNCIÓN DEFINIDA \"" + L.getOp().toString() + "\" NECESITA " + numParFun + " PARAMETRO(S)");
                return new Atom();
            }

            Hashtable<String, Atom> parameterMap = new Hashtable<String, Atom>();

            for (int i = 1; i <= numPar; i++) {
                parameterMap.put(listPar.getAtomIn(i - 1).toString(), L.getAtomIn(i));
            }

            Atom opAtom = userdeFun.get(L.getOp().toString()).List1.getAtomIn(1);

            if (opAtom.isList) {
                Atom mapList = mapParameters(parameterMap, new Atom(opAtom.List1));
                return evaluateExp(mapList);
            } else {
                return opAtom;
            }
        }

        /**
         * Método que permite mapear los parámetros de una función ingresada por el 
         * usuario al Intérprete. 
         * @param map
         * @param A
         * @return átomo con los parámetros evaluados.
         */
        private static Atom mapParameters(Hashtable<String, Atom> map, Atom A) {
            Lista evalList = new Lista(A.List1);
            evalList.isOp = A.List1.isOp;

            for (int i = 0; i < evalList.size(); i++) {
                Atom dummyAtom = evalList.getAtomIn(i);

                if (dummyAtom.isList) {
                    evalList.replaceFor(i, mapParameters(map, dummyAtom));
                } else {
                    if (map.containsKey(dummyAtom.toString())) {
                        evalList.replaceFor(i, map.get(dummyAtom.toString()));
                    }
                }
            }

            return new Atom(evalList);
        }

        /**
         * Método que permite al usuario definir una nueva función en el Intérprete.
         * @param evalList
         * @return nueva función definida en el Intérprete
         */
        private static Atom defun(Lista evalList) {
            if (evalList.size() != 4) {
                System.out.println("ERROR:  DEFUN DEBE LLEVAR 3 ARGUMENTOS (NOMBRE, PARÁMETROS, OPERACIÓN)");
                return new Atom();
            } else {
                if (evalList.getAtomIn(1).isList) {
                    System.out.println("ERROR:  DEFUN DEBE LLEVAR 3 ARGUMENTOS (NOMBRE, PARÁMETROS, OPERACIÓN)");
                    return new Atom();
                } else if (!evalList.getAtomIn(2).isList) {
                    System.out.println("ERROR:  DEFUN DEBE LLEVAR 3 ARGUMENTOS (NOMBRE, PARÁMETROS, OPERACIÓN)");
                    return new Atom();
                }
            }

            String funcNom = evalList.getAtomIn(1).toString();

            if (!validDefun(funcNom)) {
                System.out.println("ERROR:  LA FUNCION "+ funcNom +" NO ESTA DEFINIDA");
                return new Atom();
            }

            Atom funcAtom = new Atom(new Lista(evalList.subList(2,4)));
            userdeFun.put(funcNom, new Atom(new Lista(evalList.subList(2,4))));
            return new Atom(funcNom);
        }

        /**
         * Método que revisa si el usuario definió una función de forma correcta.
         * @param funcNom nombre de la funcion
         * @return True si la funcion fue definida correctamente
         */
        private static boolean validDefun(String funcNom) {
            String firstChar = Character.toString(funcNom.charAt(0));

            try {
                int dummy1 = Integer.parseInt(firstChar);
                return false;
            } catch (Exception E) {
                return true;
            }
        }

        /**
         * Método que permite realizar una división en el Intérprete.
         * @param evalList
         * @return división de los números ingresados por el usuario
         */
        private static Atom divide(Lista evalList) {
            double div = 0;

            if (evalList.getAtomIn(1).isNumber) {
                div = evalList.getAtomIn(1).number;
            } else {
                 System.out.println("ERROR: DEBE REALIZAR LA OPERACIÓN CON NÚMEROS");
            }

            for (int i = 2; i < evalList.size(); i++) {

                if (evalList.getAtomIn(i).isNumber && (evalList.getAtomIn(1).number != 0)) {
                    div /= evalList.getAtomIn(i).number;
                } else {
                    System.out.println("ERROR: DEBE REALIZAR LA OPERACIÓN CON NÚMEROS");
                    break;
                }
            }

            return new Atom(div);
        }

        /**
         * Método que permite realizar una resta en el Intérprete.
         * @param evalList
         * @return resta de los números ingresados por el usuario
         */
        private static Atom minus(Lista evalList) {
            double resta = 0;
            boolean allInt = true;

            if (evalList.getAtomIn(1).isNumber) {
                resta = evalList.getAtomIn(1).number;
            } else {
                System.out.println("ERROR: DEBE REALIZAR LA OPERACIÓN CON NÚMEROS");;
            }       

            for (int i = 2; i < evalList.size(); i++) {
                if (evalList.getAtomIn(i).isNumber) {
                    if (!evalList.getAtomIn(i).isInt()) {
                        allInt = false;
                    }
                    resta -= evalList.getAtomIn(i).getNumber();
                } else {
                    System.out.println("ERROR: DEBE REALIZAR LA OPERACIÓN CON NÚMEROS");
                    break;
                }
            }

            if (allInt) {
                return new Atom((int) resta);
            } else {
                return new Atom(resta);
            }
        }

        /**
         * Método que permite realizar una suma en el Intérprete.
         * @param evalList
         * @return suma de los números ingresados por el usuario
         */
        private static Atom sum(Lista evalList) {

            double suma = 0;
            boolean allInt = true;

            for (int i = 1; i < evalList.size(); i++) {
                if (evalList.getAtomIn(i).isNumber) {
                    if (!evalList.getAtomIn(i).isInt()) {
                        allInt = false;
                    }

                    suma += evalList.getAtomIn(i).getNumber();
                } else {
                    System.out.println("ERROR: DEBE REALIZAR LA OPERACIÓN CON NÚMEROS");
                    break;
                }
            }

            if (allInt) {
                return new Atom((int) suma);
            } else {
                return new Atom(suma);
            }
        }

        /**
         * Método que permite realizar una multiplicación en el Intérprete.
         * @param evalList
         * @return multiplicación de los números ingresados por el usuario 
         */
        private static Atom multiply(Lista evalList) {

            double mult = 1;
            boolean allInt = true;

            for (int i = 1; i < evalList.size(); i++) {
                if (evalList.getAtomIn(i).isNumber()) {
                    if (!evalList.getAtomIn(i).isInt()) 
                        allInt = false;
                    mult *= evalList.getAtomIn(i).getNumber();
                    } else {
                        System.out.println("ERROR: DEBE REALIZAR LA OPERACIÓN CON NÚMEROS");
                        break;
                    }
                }

            if (allInt) 
                return new Atom((int) mult);
            else 
                return new Atom(mult);
       }


        /**
         * Metodo que permite obtener la posición del parentesis que cierra a una expresión. 
         * @return índice del paréntesis
         */
        public static int parentesisIndex(String exp) {
            exp = exp.trim();

            if (!new Atom(exp).startsWith("(")) {
                System.out.println("ERROR: NO SE ENVIÓ UNA LISTA COMO EXPRESIÓN");
            }

            int indice = exp.indexOf('(') + 1;
            int numeroDeParentesisAbiertosPorCerrar = 1;

            while (indice < exp.length()) {
                if (exp.charAt(indice) == '(') {
                    numeroDeParentesisAbiertosPorCerrar++;
                } else if (exp.charAt(indice) == ')') {
                    numeroDeParentesisAbiertosPorCerrar--;
                    if (numeroDeParentesisAbiertosPorCerrar == 0) {
                        return indice;
                    }
                }
                indice++;
            }

            System.out.println("ERROR: VERIFIQUE LA EXPRESIÓN INGRESADA");
            return 0;
        }    

}   // FIN DE LA CLASE LISP